package com.tool.xmltool.handler;

import cn.hutool.core.text.CharSequenceUtil;
import com.tool.xmltool.cache.XmlConfigCache;
import com.tool.xmltool.entry.ComXmlConfigDO;
import com.tool.xmltool.entry.ConfigStore;
import org.dom4j.*;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.XMLWriter;

import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static com.tool.xmltool.constant.XmlConstant.DOCUMENT_NODE;
import static com.tool.xmltool.constant.XmlConstant.REMOVE_BRACKETS;

/**
 * 组装报文，将报文体map转为xml报文
 *
 * @author: zhoubin
 */
public class XmlPackHandler {

    /**
     * 报文体的map
     */
    private Map<String, Object> bodyMap;

    /**
     * xml的命名空间
     */
    private String docXmlns;

    /**
     * 报文体
     */
    private String msg;

    /**
     * 报文配置文件缓存
     */
    private ConfigStore configStore;

    public XmlPackHandler(ConfigStore configStore) {
        this.configStore = configStore;
    }

    public void initData(Map<String, Object> bodyMap) {
        initData(bodyMap, null);
    }

    /**
     * 初始化数据.
     *
     * @param bodyMap 报文体map
     * @param docXmlns xml的命名空间
     */
    public void initData(Map<String, Object> bodyMap, String docXmlns) {
        this.bodyMap = bodyMap;
        this.docXmlns = docXmlns;
    }

    public String getMsg() {
        return msg;
    }

    /**
     * 将报文的bean转为报文体
     * @return
     */
    public void map2Xml() throws IOException {
        // key为业务语义，值为xml配置对象
        List<ComXmlConfigDO> xmlConfigList = configStore.getConfigList();
        // 根据map用dom4j创建Document
        Document doc = DocumentHelper.createDocument();
        // 从bodyMap中获取值，拼装 document
        packDocument(bodyMap, xmlConfigList, doc);
        //格式化
        OutputFormat format = OutputFormat.createPrettyPrint();
        format.setExpandEmptyElements(false);
        format.setTrimText(false);
        StringWriter out = new StringWriter();
        XMLWriter writer = new XMLWriter(out, format);
        try {
            writer.write(doc);
            writer.flush();
        } catch (IOException e) {
            throw e;
        }

        this.msg = out.toString();

        // Document节点添加xmlns属性
        if (CharSequenceUtil.isNotBlank(docXmlns)) {
            this.msg = msg.replaceFirst(DOCUMENT_NODE,
                    this.docXmlns);
        }
    }


    /**
     * 使用递归拼装 document
     * @param bodyMap 除循环域外的报文体
     * @param xmlConfigList xml配置list
     * @param doc xml的document
     */
    private void packDocument(Map<String, Object> bodyMap, List<ComXmlConfigDO> xmlConfigList,
                              Branch doc) {
        for (ComXmlConfigDO xmlConfig : xmlConfigList) {
            // 从报文体的map中获取值
            Object nodeText = bodyMap.get(xmlConfig.getBusinessSemantics());
            // 如果值为null，则继续循环下一个节点
            if (nodeText == null) {
                continue;
            }
            if (nodeText instanceof String) {
                // 当获取到的值是String类型时，表示该节点下没有子节点。可以直接创建该节点
                // 获取document的xpath路径, 并去除中括号的序号
                String uniquePath = doc.getUniquePath();
                uniquePath = uniquePath.replaceAll(REMOVE_BRACKETS, "");
                String nodeVal = (String) nodeText;
                // 获取到本节点与document的相对的xpath
                String xpath = xmlConfig.getNodePath().substring(uniquePath.length());
                // 创建节点
                createEle(doc, xpath, nodeVal);

            } else {
                // 如果获取到的值不是String类型，则一定是list类型的。也表示该节点下有循环节点。需要做特殊处理
                List nodeList = (ArrayList) nodeText;
                for (int i = 0; i < nodeList.size(); i++) {
                    Map map = (HashMap) nodeList.get(i);
                    if (i == 0) {
                        // 如果是循环节点的第一个节点时，由于此时的循环节点不存在，不存在多个循环节点有下标的情况，所以可以直接创建节点
                        packDocument(map, xmlConfig.getAppendList(), doc);
                    } else {
                        // 如果不是循环节点的第一个节点时，则表示已经有了循环节点，则则可以通过第一个循环节点获取到父节点，
                        // 在循环节点的父节点的基础上新创建一个循环节点。
                        // 获取doc的xpath 例: /Document/PrtryMsg/PrtryData/Data/DscrdInf/a[2]
                        String uniquePath = doc.getUniquePath();
                        // 去除xpath的中括号 例: /Document/PrtryMsg/PrtryData/Data/DscrdInf/a
                        String noBrackets = uniquePath.replaceAll(REMOVE_BRACKETS, "");
                        // 获取当前需要拼接节点的xpath 例: /Document/PrtryMsg/PrtryData/Data/DscrdInf/a/b
                        String nodePath = xmlConfig.getNodePath();
                        // 拼接出当前循环的第一个循环节点真实的xpath，带有标号的 例: /Document/PrtryMsg/PrtryData/Data/DscrdInf/a[2]/b
                        String relPath = uniquePath + nodePath.substring(noBrackets.length());
                        // 由于这必定不是第一个，所以能通过xpath获取到节点
                        // 这个节点是当前循环的第一个节点
                        XPath xPath = DocumentHelper.createXPath(relPath);
                        Node node = xPath.selectSingleNode(doc);
                        // 获取父节点并在父节点后添加一个循环节点，并将该循环节点作为下一次递归的父节点
                        Element parent = node.getParent();
                        Element element = parent.addElement(nodePath.substring(nodePath.lastIndexOf("/") + 1));
                        packDocument(map, xmlConfig.getAppendList(), element);
                    }
                }
            }
        }
    }

    /**
     * 创建一个新的节点，并将值存入节点中
     * @param doc xml的document
     * @param xpath 对应节点的xpath
     * @param value 节点的值
     */
    private void createEle(Branch doc, String xpath, String value) {
        if (CharSequenceUtil.isEmpty(value)) {
            return;
        }
        if (xpath.contains("@")) {
            // 如果xpath包含@，则表示该xpath是一个属性的xpath
            String key1 = xpath.substring(0, xpath.indexOf("@") - 1);
            String attr = xpath.substring(xpath.indexOf("@") + 1);
            Element ele = DocumentHelper.makeElement(doc, key1);
            ele.addAttribute(attr, value);
        } else {
            // 直接创建一个新节点
            if (CharSequenceUtil.isEmpty(xpath)) {
                doc.setText(value);
            } else {
                Element ele = DocumentHelper.makeElement(doc, xpath);
                ele.setText(value);
            }

        }
    }
}
